
package org.apache.solr.schema;

import java.io.IOException;
import java.text.*;
import java.util.*;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.DocTermsIndexDocValues;
import org.apache.lucene.queries.function.valuesource.FieldCacheSource;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.UnicodeUtil;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.DateUtil;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.util.DateMathParser;

// TODO: make a FlexibleDateField that can accept dates in multiple
// formats, better for human entered dates.
// TODO: make a DayField that only stores the day?

/**
 * FieldType that can represent any Date/Time with millisecond precision.
 * <p>
 * Date Format for the XML, incoming and outgoing:
 * </p>
 * <blockquote>
 * A date field shall be of the form 1995-12-31T23:59:59Z
 * The trailing "Z" designates UTC time and is mandatory
 * (See below for an explanation of UTC).
 * Optional fractional seconds are allowed, as long as they do not end
 * in a trailing 0 (but any precision beyond milliseconds will be ignored).
 * All other parts are mandatory.
 * </blockquote>
 * <p>
 * This format was derived to be standards compliant (ISO 8601) and is a more
 * restricted form of the
 * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime-canonical-representation">canonical
 * representation of dateTime</a> from XML schema part 2.  Examples...
 * </p>
 * <ul>
 *   <li>1995-12-31T23:59:59Z</li>
 *   <li>1995-12-31T23:59:59.9Z</li>
 *   <li>1995-12-31T23:59:59.99Z</li>
 *   <li>1995-12-31T23:59:59.999Z</li>
 * </ul>
 * <p>
 * Note that DateField is lenient with regards to parsing fractional
 * seconds that end in trailing zeros and will ensure that those values
 * are indexed in the correct canonical format.
 * </p>
 * <p>
 * This FieldType also supports incoming "Date Math" strings for computing
 * values by adding/rounding internals of time relative either an explicit
 * datetime (in the format specified above) or the literal string "NOW",
 * ie: "NOW+1YEAR", "NOW/DAY", "1995-12-31T23:59:59.999Z+5MINUTES", etc...
 * -- see {@link DateMathParser} for more examples.
 * </p>
 *
 * <p>
 * Explanation of "UTC"...
 * </p>
 * <blockquote>
 * "In 1970 the Coordinated Universal Time system was devised by an
 * international advisory group of technical experts within the International
 * Telecommunication Union (ITU).  The ITU felt it was best to designate a
 * single abbreviation for use in all languages in order to minimize
 * confusion.  Since unanimous agreement could not be achieved on using
 * either the English word order, CUT, or the French word order, TUC, the
 * acronym UTC was chosen as a compromise."
 * </blockquote>
 *
 *
 * @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML schema part 2</a>
 *
 */
public class DateField extends PrimitiveFieldType {

    public static TimeZone UTC = TimeZone.getTimeZone("UTC");
    /**
     * Fixed TimeZone (UTC) needed for parsing/formating Dates in the canonical representation.
     */
    protected static final TimeZone CANONICAL_TZ = UTC;
    /**
     * Fixed Locale needed for parsing/formating Milliseconds in the canonical representation.
     */
    protected static final Locale CANONICAL_LOCALE = Locale.ROOT;
    // The XML (external) date format will sort correctly, except if
    // fractions of seconds are present (because '.' is lower than 'Z').
    // The easiest fix is to simply remove the 'Z' for the internal
    // format.
    protected static String NOW = "NOW";
    protected static char Z = 'Z';
    private static char[] Z_ARRAY = new char[]{Z};

    @Override
    public String toInternal(String val) {
        return toInternal(parseMath(null, val));
    }

    /**
     * Parses a String which may be a date (in the standard format) followed by
     * an optional math expression.
     *
     * @param now an optional fixed date to use as "NOW" in the DateMathParser
     * @param val the string to parse
     */
    public Date parseMath(Date now, String val) {

        String math = null;
        final DateMathParser p = new DateMathParser();

        if (null != now) {
            p.setNow(now);
        }

        if (val.startsWith(NOW)) {
            math = val.substring(NOW.length());
        }
        else {
            final int zz = val.indexOf(Z);
            if (0 < zz) {
                math = val.substring(zz + 1);
                try {
                    // p.setNow(toObject(val.substring(0,zz)));
                    p.setNow(parseDate(val.substring(0, zz + 1)));
                } catch (ParseException e) {
                    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Date in Date Math String:'" + val + '\'', e);
                }
            }
            else {
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Date String:'" + val + '\'');
            }
        }

        if (null == math || math.equals("")) {
            return p.getNow();
        }

        try {
            return p.parseMath(math);
        }
        catch (ParseException e) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Date Math String:'" + val + '\'', e);
        }
    }

    @Override
    public IndexableField createField(SchemaField field, Object value, float boost) {
        // Convert to a string before indexing
        if (value instanceof Date) {
            value = toInternal((Date) value) + Z;
        }
        return super.createField(field, value, boost);
    }

    public String toInternal(Date val) {
        return formatDate(val);
    }

    @Override
    public String indexedToReadable(String indexedForm) {
        return indexedForm + Z;
    }

    @Override
    public CharsRef indexedToReadable(BytesRef input, CharsRef charsRef) {
        UnicodeUtil.UTF8toUTF16(input, charsRef);
        charsRef.append(Z_ARRAY, 0, 1);
        return charsRef;
    }

    @Override
    public String toExternal(IndexableField f) {
        return indexedToReadable(f.stringValue());
    }

    public Date toObject(String indexedForm) throws java.text.ParseException {
        return parseDate(indexedToReadable(indexedForm));
    }

    @Override
    public Date toObject(IndexableField f) {

        try {
            return parseDate(toExternal(f));
        }
        catch (ParseException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public SortField getSortField(SchemaField field, boolean reverse) {
        return getStringSort(field, reverse);
    }

    @Override
    public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
        writer.writeDate(name, toExternal(f));
    }

    /**
     * Thread safe method that can be used by subclasses to format a Date using
     * the Internal representation.
     */
    protected String formatDate(Date d) {
        return fmtThreadLocal.get().format(d);
    }

    /**
     * Return the standard human readable form of the date
     */
    public static String formatExternal(Date d) {
        return fmtThreadLocal.get().format(d) + 'Z';
    }

    /**
     * @see #formatExternal
     */
    public String toExternal(Date d) {
        return formatExternal(d);
    }

    /**
     * Thread safe method that can be used by subclasses to parse a Date that is
     * already in the internal representation
     */
    public static Date parseDate(String s) throws ParseException {
        return fmtThreadLocal.get().parse(s);
    }

    /**
     * Parse a date string in the standard format, or any supported by
     * DateUtil.parseDate
     */
    public Date parseDateLenient(String s, SolrQueryRequest req) throws ParseException {
        // request could define timezone in the future
        try {
            return fmtThreadLocal.get().parse(s);
        }
        catch (Exception e) {
            return DateUtil.parseDate(s);
        }
    }

    /**
     * Parses a String which may be a date followed by an optional math
     * expression.
     *
     * @param now an optional fixed date to use as "NOW" in the DateMathParser
     * @param val the string to parse
     */
    public Date parseMathLenient(Date now, String val, SolrQueryRequest req) {

        String math = null;
        final DateMathParser p = new DateMathParser();

        if (null != now) {
            p.setNow(now);
        }

        if (val.startsWith(NOW)) {
            math = val.substring(NOW.length());
        }
        else {
            final int zz = val.indexOf(Z);
            if (0 < zz) {
                math = val.substring(zz + 1);
                try {
                    // p.setNow(toObject(val.substring(0,zz)));
                    p.setNow(parseDateLenient(val.substring(0, zz + 1), req));
                }
                catch (ParseException e) {
                    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Date in Date Math String:'" + val + '\'', e);
                }
            }
            else {
                throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Date String:'" + val + '\'');
            }
        }

        if (null == math || math.equals("")) {
            return p.getNow();
        }

        try {
            return p.parseMath(math);
        }
        catch (ParseException e) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid Date Math String:'" + val + '\'', e);
        }
    }
    /**
     * Thread safe DateFormat that can <b>format</b> in the canonical ISO8601
     * date format, not including the trailing "Z" (since it is left off in the
     * internal indexed values)
     */
    private final static ThreadLocalDateFormat fmtThreadLocal = new ThreadLocalDateFormat(new ISO8601CanonicalDateFormat());

    private static class ISO8601CanonicalDateFormat extends SimpleDateFormat {

        protected NumberFormat millisParser = NumberFormat.getIntegerInstance(CANONICAL_LOCALE);
        protected NumberFormat millisFormat = new DecimalFormat(".###",
                new DecimalFormatSymbols(CANONICAL_LOCALE));

        public ISO8601CanonicalDateFormat() {
            super("yyyy-MM-dd'T'HH:mm:ss", CANONICAL_LOCALE);
            this.setTimeZone(CANONICAL_TZ);
        }

        @Override
        public Date parse(String i, ParsePosition p) {
            /* delegate to SimpleDateFormat for easy stuff */
            Date d = super.parse(i, p);
            int milliIndex = p.getIndex();
            /* worry about the milliseconds ourselves */
            if (null != d && -1 == p.getErrorIndex() && milliIndex + 1 < i.length() && '.' == i.charAt(milliIndex)) {
                p.setIndex(++milliIndex); // NOTE: ++ to chomp '.'
                Number millis = millisParser.parse(i, p);
                if (-1 == p.getErrorIndex()) {
                    int endIndex = p.getIndex();
                    d = new Date(d.getTime() + (long) (millis.doubleValue() * Math.pow(10, (3 - endIndex + milliIndex))));
                }
            }
            return d;
        }

        @Override
        public StringBuffer format(Date d, StringBuffer toAppendTo, FieldPosition pos) {

            /* delegate to SimpleDateFormat for easy stuff */
            super.format(d, toAppendTo, pos);
            /* worry aboutthe milliseconds ourselves */
            long millis = d.getTime() % 1000l;
            if (0L == millis) {
                return toAppendTo;
            }
            if (millis < 0L) {
                // original date was prior to epoch
                millis += 1000L;
            }
            int posBegin = toAppendTo.length();
            toAppendTo.append(millisFormat.format(millis / 1000d));
            if (DateFormat.MILLISECOND_FIELD == pos.getField()) {
                pos.setBeginIndex(posBegin);
                pos.setEndIndex(toAppendTo.length());
            }
            return toAppendTo;
        }

        @Override
        public DateFormat clone() {

            ISO8601CanonicalDateFormat c = (ISO8601CanonicalDateFormat) super.clone();
            c.millisParser = NumberFormat.getIntegerInstance(CANONICAL_LOCALE);
            c.millisFormat = new DecimalFormat(".###", new DecimalFormatSymbols(CANONICAL_LOCALE));
            return c;
        }
    }

    private static class ThreadLocalDateFormat extends ThreadLocal<DateFormat> {

        DateFormat proto;

        public ThreadLocalDateFormat(DateFormat d) {
            super();
            proto = d;
        }

        @Override
        protected DateFormat initialValue() {
            return (DateFormat) proto.clone();
        }
    }

    @Override
    public ValueSource getValueSource(SchemaField field, QParser parser) {
        field.checkFieldCacheSource(parser);
        return new DateFieldSource(field.getName(), field.getType());
    }

    /**
     * DateField specific range query
     */
    public Query getRangeQuery(QParser parser, SchemaField sf, Date part1, Date part2, boolean minInclusive, boolean maxInclusive) {

        return TermRangeQuery.newStringRange(
                sf.getName(),
                part1 == null ? null : toInternal(part1),
                part2 == null ? null : toInternal(part2),
                minInclusive, maxInclusive);
    }
}

class DateFieldSource extends FieldCacheSource {
    // NOTE: this is bad for serialization... but we currently need the fieldType for toInternal()

    FieldType ft;

    public DateFieldSource(String name, FieldType ft) {
        super(name);
        this.ft = ft;
    }

    @Override
    public String description() {
        return "date(" + field + ')';
    }

    @Override
    public FunctionValues getValues(Map context, AtomicReaderContext readerContext) throws IOException {

        return new DocTermsIndexDocValues(this, readerContext, field) {
            @Override
            protected String toTerm(String readableValue) {
                // needed for frange queries to work properly
                return ft.toInternal(readableValue);
            }

            @Override
            public float floatVal(int doc) {
                return (float) intVal(doc);
            }

            @Override
            public int intVal(int doc) {
                int ord = termsIndex.getOrd(doc);
                return ord;
            }

            @Override
            public long longVal(int doc) {
                return (long) intVal(doc);
            }

            @Override
            public double doubleVal(int doc) {
                return (double) intVal(doc);
            }

            @Override
            public String strVal(int doc) {

                int ord = termsIndex.getOrd(doc);
                if (ord == 0) {
                    return null;
                }
                else {
                    final BytesRef br = termsIndex.lookup(ord, spare);
                    return ft.indexedToReadable(br, spareChars).toString();
                }
            }

            @Override
            public Object objectVal(int doc) {

                int ord = termsIndex.getOrd(doc);
                if (ord == 0) {
                    return null;
                }
                else {
                    final BytesRef br = termsIndex.lookup(ord, new BytesRef());
                    return ft.toObject(null, br);
                }
            }

            @Override
            public String toString(int doc) {
                return description() + '=' + intVal(doc);
            }
        };
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof DateFieldSource && super.equals(o);
    }
    private static int hcode = DateFieldSource.class.hashCode();

    @Override
    public int hashCode() {
        return hcode + super.hashCode();
    }
}
